﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ValidationLibrary;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;

namespace WebFormsBinder
{
    public class JSGenerator<TEntity> : IJavascriptGenerator<TEntity>
    {
        Binder<TEntity> Binder { get; set; }
        TEntity Entity { get; set; }
        ModelValidator<TEntity> ModelValidator { get; set; }

        Dictionary<ExpressionType, string> _binaryOperators;

        public JSGenerator(ModelValidator<TEntity> modelValidator, Binder<TEntity> binder, TEntity entity)
        {
            ModelValidator = modelValidator;
            Binder = binder;
            Entity = entity;

            _binaryOperators = new Dictionary<ExpressionType, string>();
            _binaryOperators.Add(ExpressionType.Equal, "===");
            _binaryOperators.Add(ExpressionType.GreaterThan, ">");
            _binaryOperators.Add(ExpressionType.GreaterThanOrEqual, ">=");
            _binaryOperators.Add(ExpressionType.LessThan, "<");
            _binaryOperators.Add(ExpressionType.LessThanOrEqual, "<=");
            _binaryOperators.Add(ExpressionType.NotEqual, "!==");

            _binaryOperators.Add(ExpressionType.Add, "+");
            _binaryOperators.Add(ExpressionType.Divide, "/");
            _binaryOperators.Add(ExpressionType.Modulo, "%");
            _binaryOperators.Add(ExpressionType.Multiply, "*");
            _binaryOperators.Add(ExpressionType.Power, "^");
            _binaryOperators.Add(ExpressionType.Subtract, "-");

            _binaryOperators.Add(ExpressionType.Coalesce, "||");

            _binaryOperators.Add(ExpressionType.AndAlso, "&&");
            _binaryOperators.Add(ExpressionType.OrElse, "||");
        }

        public List<string> GetJavascript()
        {
            List<string> list = new List<string>();
            foreach (var rule in ModelValidator.Rules)
            {
                list.Add(Javascript(rule));
            }
            return list;
        }

        public JSValidationRule<TEntity> Generate(ValidationRule<TEntity> validationRule)
        {
            throw new NotImplementedException();
            //return new JSValidationRule<TEntity>(Javascript(validationRule), validationRule.Message, null);
        }

        public IEnumerable<JSValidationRule<TEntity>> GenerateRules()
        {
            List<JSValidationRule<TEntity>> rules = new List<JSValidationRule<TEntity>>();
            foreach (var rule in ModelValidator.Rules)
            {
                rules.Add(Generate(rule));
            }
            return rules;
        }

        string Javascript(ValidationRule<TEntity> validationRule)
        {
            Expression<Func<TEntity, bool>> ruleExpression = validationRule.RuleExpression;

            return ProcessLambdaExpression(ruleExpression);
        }
        
        string ProcessLambdaExpression(LambdaExpression expression)
        {
            return ProcessExpression(expression.Body);
        }

        string ProcessExpression(Expression expression)
        {
            if (expression is BinaryExpression)
                return ProcessBinaryExpression(expression as BinaryExpression);
            else if (expression is MethodCallExpression)
                return ProcessMethodCallExpression(expression as MethodCallExpression);
            else if (expression is UnaryExpression)
                return ProcessUnaryExpression(expression as UnaryExpression);
            else if (expression is ConditionalExpression)
                return ProcessConditionalExpression(expression as ConditionalExpression);
            else if (expression is MemberExpression)
                return ProcessMemberExpression(expression as MemberExpression);
            else if (expression is ConstantExpression)
                return ProcessConstantExpression(expression as ConstantExpression);
            else if (expression is NewExpression)
                return ProcessNewExpression(expression as NewExpression);

            throw new NotImplementedException(string.Format("Unhandled expression type: {0}", expression.NodeType));
        }

        private string ProcessNewExpression(NewExpression newExpression)
        {
            object result = Expression.Lambda(newExpression, null).Compile().DynamicInvoke(null);
            return GetJSValue(result);
        }

        private string ProcessConstantExpression(ConstantExpression constantExpression)
        {
            object value = constantExpression.Value;
            return GetJSValue(value);
        }

        private static string GetJSValue(object value)
        {
            if (value == null)
                return "null";
            if (value.GetType() == typeof(bool))
                return ((bool)value) ? "true" : "false";
            if (value.GetType() == typeof(string))
                return string.Format("\"{0}\"", value);
            if (value.GetType() == typeof(int) || value.GetType() == typeof(decimal))
                return value.ToString();
            if (value.GetType() == typeof(DateTime))
            {
                DateTime date = (DateTime)value;
                return string.Format("new Date({0},{1},{2},{3},{4},{5})",
                    date.Year, date.Month, date.Day,
                    date.Hour, date.Minute, date.Second);
            }

            throw new NotImplementedException(string.Format("Unhandled constant value type: {0}", value.GetType()));
        }

        private string ProcessMemberExpression(MemberExpression memberExpression)
        {
            if (memberExpression.Member.DeclaringType != typeof(TEntity))
                throw new NotSupportedException(string.Format("Reference to class {0} cannot be used when binding class {1}", memberExpression.Member.DeclaringType.Name, typeof(TEntity).Name));

            PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo;
            if (propertyInfo == null)
                throw new NotSupportedException(string.Format("Can only reference properties, not methods or fields: {1}.{0}", memberExpression.Member.DeclaringType.Name, memberExpression.Member.Name));

            AbstractBinding<TEntity> propertyBinding = Binder.Bindings.SingleOrDefault(x=>
                x.Property == propertyInfo);

            if (propertyBinding != null)
            {
                return string.Format("({0})", propertyBinding.JSValue);
            }
            else
            {
                object value = Expression.Lambda(memberExpression).Compile().DynamicInvoke();
                //object value = propertyInfo.GetGetMethod().Invoke(Entity, null);
                return GetJSValue(value);
            }
        }

        private string ProcessMethodCallExpression(MethodCallExpression methodCallExpression)
        {
            if (methodCallExpression.Method.DeclaringType == typeof(string))
            {
                if (methodCallExpression.Method.IsStatic)
                {
                    if (methodCallExpression.Method.Name == "IsNullOrEmpty")
                    {
                        string operand = ProcessExpression(methodCallExpression.Arguments[0]);
                        return string.Format("({0}.length > 0)", operand);
                    }
                }
                else
                {
                    string operand = ProcessExpression(methodCallExpression.Object);
                    string arg0, arg1;

                    switch (methodCallExpression.Method.Name)
                    {
                        case "Contains":
                            if (methodCallExpression.Arguments.Count != 1)
                                throw new NotSupportedException("Method call not supported: string.Contains(char, IEqualityComparer<char>)");
                            arg0 = ProcessExpression(methodCallExpression.Arguments[0]);
                            return string.Format("({0}.indexOf({1}) !== -1)", operand, arg0);
                        case "EndsWith":
                            if (methodCallExpression.Arguments.Count != 1)
                                throw new NotSupportedException("Method call string.EndsWith only supported with single argument");
                            arg0 = ProcessExpression(methodCallExpression.Arguments[0]);
                            return string.Format("({0}.match(/{1}$/))", operand, arg0);
                        case "IndexOf":
                            if (methodCallExpression.Arguments.Count != 1)
                                throw new NotSupportedException("Method call string.IndexOf only supported with single argument");
                            arg0 = ProcessExpression(methodCallExpression.Arguments[0]);
                            return string.Format("{0}.indexOf({1})", operand, arg0);
                        case "StartsWith":
                            if (methodCallExpression.Arguments.Count != 1)
                                throw new NotSupportedException("Method call string.StartsWith only supported with single argument");
                            arg0 = ProcessExpression(methodCallExpression.Arguments[0]);
                            return string.Format("({0}.match(/^{1}/))", operand, arg0);
                        case "Substring":
                            arg0 = ProcessExpression(methodCallExpression.Arguments[0]);
                            if (methodCallExpression.Arguments.Count == 1)
                                return string.Format("{0}.substring({1})", operand, arg0);
                            else
                            {
                                arg1 = ProcessExpression(methodCallExpression.Arguments[1]);
                                int iArg1Calculated = int.Parse(arg0) + int.Parse(arg1);
                                return string.Format("{0}.substring({1},{2})", operand, arg0, iArg1Calculated);
                            }
                        case "ToLower":
                            if (methodCallExpression.Arguments.Count != 0)
                                throw new NotSupportedException("Method call not supported: string.ToLower(CultureInfo)");
                            return string.Format("{0}.toLowerCase()", operand);
                        case "ToUpper":
                            if (methodCallExpression.Arguments.Count != 0)
                                throw new NotSupportedException("Method call not supported: string.ToUpper(CultureInfo)");
                            return string.Format("{0}.toUpperCase()", operand);
                    }
                }

                throw new NotSupportedException(string.Format("Method call not supported: string.{0}", methodCallExpression.Method.Name));
            }
            else if (methodCallExpression.Method.DeclaringType == typeof(Regex))
            {
                if (methodCallExpression.Method.IsStatic &&
                    methodCallExpression.Method.Name == "IsMatch")
                {
                    string operand = ProcessExpression(methodCallExpression.Arguments[0]);
                    string pattern = ProcessExpression(methodCallExpression.Arguments[1]);
                    string modifier = string.Empty;
                    if (methodCallExpression.Arguments.Count == 3)
                    {
                        throw new NotSupportedException("No regex modifiers are supported.");
                    }
                    return string.Format("{0}.match(/{1}/{2})", operand, pattern, modifier);
                }

                throw new NotSupportedException("Only the static Regex.IsMatch method is supported");
            }

            throw new NotImplementedException();
        }

        private string ProcessConditionalExpression(ConditionalExpression conditionalExpression)
        {
            string test = ProcessExpression(conditionalExpression.Test);
            string ifTrue = ProcessExpression(conditionalExpression.IfTrue);
            string ifFalse = ProcessExpression(conditionalExpression.IfFalse);

            return string.Format("({0} ? {1} : {2})", test, ifTrue, ifFalse);
        }

        private string ProcessUnaryExpression(UnaryExpression unaryExpression)
        {
            if (unaryExpression.NodeType == ExpressionType.Negate)
            {
                string operand = ProcessExpression(unaryExpression.Operand);
                return string.Format("-({0})", operand);
            }
            if (unaryExpression.NodeType == ExpressionType.Not)
            {
                string operand = ProcessExpression(unaryExpression.Operand);
                return string.Format("!({0})", operand);
            }
            throw new NotSupportedException(string.Format("Unhandled UnaryExpression type: {0}", unaryExpression.NodeType));
        }

        private string ProcessBinaryExpression(BinaryExpression binaryExpression)
        {
            if (_binaryOperators.ContainsKey(binaryExpression.NodeType))
            {
                string left = ProcessExpression(binaryExpression.Left);
                string op = _binaryOperators[binaryExpression.NodeType];
                string right = ProcessExpression(binaryExpression.Right);

                return string.Format("({0} {1} {2})", left, op, right);
            }
            throw new NotSupportedException(string.Format("Unhandled BinaryExpression type: {0}", binaryExpression.NodeType));
        }
    }
}
